package ml.practicum;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.List;
import java.util.Properties;

import ml.practicum.feature.LVFeature;
import ml.practicum.learn.BasicPerceptron;
import ml.practicum.learn.Learner;
import ml.practicum.learn.PerceptronFactory;
import ml.practicum.learn.PerceptronLearner;
import ml.practicum.learn.PerceptronRepresentation;
import ml.practicum.learn.Representation;
import ml.practicum.logistic.BasicLV;
import ml.practicum.table.DynamicHeadedTable;
import ml.practicum.table.HeadedTable;
import ml.practicum.table.HeadedTableFactory;
import ml.practicum.table.HeaderFactory;
import ml.practicum.util.Percist;
/**
 * Main class of Mlp project handles the commanline arguments and load defaults
 * properties and files
 * @author Joscha
 *
 */
public class Main {
	private String[] args;
	private Properties properties;
	
	private HeadedTable<String,String> data;
	private HeadedTable<LVFeature<String>,String> processed;
	private BasicPerceptron<BasicLV> perceptron;
	private double score; 
	private Learner<BasicPerceptron<BasicLV>,HeadedTable<LVFeature<String>,String>> learner;
	
	/**
	 * Default constructor, wil only be used of not used as Main class
	 */
	public Main(){
		this(new String [0]);
	}
	/**
	 * Constructor loading an argument line to execute potentially overriding 
	 * the default arguments loaded from the Main.properties file
	 *  
	 * @param args	 the command arguments
	 */
	public Main(String[] args){
		properties = loadProperties();
		this.args = properties.getProperty("args").split(" ");
		this.args = loadArgs(args);
		
		data = null;
		processed = null;
		perceptron = null;
		learner = new PerceptronLearner();
		score = 0;
	}
	
	/**
	 * Load properties from properties resource file accompanying the class
	 * (Main.properties) in this case
	 * @return the properties from the file
	 */
	private Properties loadProperties(){
		Properties result = new Properties(); 
		try{
			result.load(this.getClass().getResourceAsStream(this.getClass().getSimpleName() + ".properties"));
		}catch(IOException e){
			e.printStackTrace();
		}
		return result;
	}
	
	/**
	 * Store properties to resource file accompanying class,
	 * This doesn't work at the moment. (could be fixed, needs testing) 
	 * Should be easy to fix you can get a File based on a URL if I'm not mistaken
	 */
	private void storeProperties(){
		//FIXME: URL werkt niet voor output
		//Could be fixed, needs testing
		try{
		URL resourceURL = this.getClass().getResource(this.getClass().getSimpleName() + ".properties");
			properties.store(new FileOutputStream(resourceURL.getFile()), "");
		}catch(IOException e){
			e.printStackTrace();
		}
	}
	
	/**
	 * load arguments and overrides the default arguments from the property files
	 * @param args
	 * @return
	 */
	String[] loadArgs(String[] args){
		if (args.length<this.args.length){
			for(int i = 0;i<args.length;i++){
				this.args[i]=args[i];
			}
			return this.args;
		}else{
			return args;
		}
	}
	/**
	 * set name property currently associated with the loaded perceptron and header 
	 * @param name the new name to associate
	 */
	private void setName(String name){
		// if there's a name set in in the properties
		if (name != null && !name.equals(""))properties.setProperty("name", name);
	}
	
	/**
	 * Create a new table with a header containing the meta data for processing
	 * and load the data into the table
	 * <p>NB. the data is actually not processed or transformed yet. 
	 * So the name is a little misleading,
	 * </b>
	 * @param header 
	 */
	private void setProcessedTable(List<LVFeature<String>> header){
		// make header and make the processed table
		processed = new DynamicHeadedTable<LVFeature<String>,String>(header);
		processed.setRows(data.getRows());	
	}
	
	/**
	 * make new header and perceptron based on a properties file
	 * @param propertiesFilename 	path of a header properties file
	 */
	void make(String propertiesFilename){
		File propertiesFile = new File(propertiesFilename);
		setProcessedTable(HeaderFactory.create(propertiesFile, data));
		perceptron = new PerceptronFactory<BasicLV>().create(processed);
	}
	
	/**
	 * save the perceptron and header information
	 * @param name path and name of a perceptron and header file excluding the extension
	 */
	void save(String name){
		setName(name);
		
		// store header
		File headerFile = new File(properties.getProperty("name") + ".header"); 
		Percist.store(headerFile, processed.getHeader());
		
		// store perceptron
		File perceptronFile = new File(properties.getProperty("name") + ".perceptron"); 
		Percist.store(perceptronFile, perceptron);
	}

	/**
	 * load a perceptron and header file
	 * @param name path and name of a perceptron and header file excluding the extension
	 */
	@SuppressWarnings("unchecked")
	void load(String name){
		setName(name);
		
		// load header
		File headerFile = new File(properties.getProperty("name") + ".header"); 
		List<LVFeature<String>> header = (List<LVFeature<String>>) Percist.load(headerFile);
		setProcessedTable(header);

		// load perceptron
		File perceptronFile = new File(properties.getProperty("name") + ".perceptron"); 
		perceptron = (BasicPerceptron<BasicLV>) Percist.load(perceptronFile);
	}
	
	/**
	 * train the loaded perceptron x times
	 * @param x		the amount of time to run the learning algorithm on the dataset.
	 */
	void train(int x){
		if(perceptron != null && processed != null){
			for (int i = 0; i < x ; i++){
				perceptron = learner.learn(perceptron,processed);
			}
		}	
	}
	
	/**
	 * 
	 * @param instr
	 * @param nr
	 * @return
	 */
	int validate(String instr,int nr){
		double score = new PerceptronLearner().test(perceptron,processed);
		System.out.printf("Results on the dataset: %f\n",score);
		if (score > this.score && instr!= null && !instr.equals("") ){
			char commando = instr.charAt(1);
			String attribute = instr.substring(2);
			nr = execute(commando,attribute,nr);
		}
		this.score = score;
		return nr;
	}
	
	void output(String path){
		OutputStream output = System.out;
		if (path!= null && !path.equals("")){
			try{
				File file = new File(path);
				file.createNewFile();
				output = new FileOutputStream(file);
			System.out.printf("Writing output to: '%s'\n",file.getAbsolutePath());
			}catch(IOException e){
				e.printStackTrace();
			}
		}
		PrintStream printStream = new PrintStream(output);
		print(printStream,new PerceptronLearner().calculate(perceptron, processed).getColumn(0));
	}
	
	void print(PrintStream out,List<String> list){
		for(String item:list){
			out.println(item);
		}
	}
	
	void perceptronPrint(String path){
		OutputStream output = System.out;
		if (path!= null && !path.equals("")){
			try{
				output = new FileOutputStream(path);
			}catch(IOException e){
				e.printStackTrace();
			}
		}
		print(new PrintStream(output));
	}
	
	void print(PrintStream output){
		Representation<BasicPerceptron<BasicLV>,HeadedTable<LVFeature<String>,String>> 
		representation = new PerceptronRepresentation();
		representation.setPerceptron(perceptron);
		representation.setProcessed(processed);		
			output.print(representation);
	}
	
	void setDefault(String name){
		setName(name);
		storeProperties();
	}
	
	void clear(int x){
		perceptron.randomize(-x, x, perceptron.getWeights().size());
	}
	void rename(String name){
		setName(name);
	}
	
	@SuppressWarnings("unchecked")
	void algorithm(String className){
		try {
			learner = (Learner<BasicPerceptron<BasicLV>,HeadedTable<LVFeature<String>,String>>)
			Class.forName(className).getConstructor().newInstance();
		} catch (IllegalArgumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (SecurityException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InstantiationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	int parseInt(String x, int defaultInt){
		int result = defaultInt;
		try{
			result = Integer.parseInt(x);
		}catch(Exception e){
			// do nothing;
		}
		return result;
	}
	
	int execute(char commando, String attribute,int instruction){
		switch(commando){
		case 'm'://make (makes new header and perceptron NB.does not save)
			make(attribute); //attribute = path/basename of header settings properties file
		break;	
			
		case 'l'://load (loads header from attribute path/basename)
			load(attribute);// if attribute is nothing then default is loaded from properties
		break;
		
		case 't'://train (trains perceptron)
			train(parseInt(attribute,1));//attribute is times to train NB. -t4 trains 4 times
		break;
	
		case 'v'://validate (returns output on how well the perceptron is scoring on the data set.
			return validate(attribute,instruction);// is not used, might be used for crossvalidation settings in the future.
		
		case 'o'://output (outputs calculated output of entire dataset)
			output(attribute);// attribute is outputstream path > default is System.out
		break;
		case 's'://save (this stores the header and perceptron under the name currently set
			save(attribute);//attribute = the name to set, if empty set name or default is used 
		break;
		
		case 'd'://default (sets default to current name/path)
			setDefault(attribute);////attribute = the name to set, if empty set name or default is used 
		break;
		
		case 'e'://edit
			//TODO: editor for header
			//edit(attribute);
		break;
		
		case 'g'://goto
			return instruction + parseInt(attribute,0);
		
		case 'c'://clear (randomizes the perceptron weights)
			clear(parseInt(attribute,1)); //attribute limits the random weight range (1 is default)
		break;
		//FIXME: consider this function is not really needed
		case 'r'://rename sets name to attribute value 
			rename(attribute);// if empty current or default is kept
		break;
		
		case 'a'://algorithm class selection
			algorithm(attribute);
		break;
		
		case 'p'://represents header and perceptron value for analysis
			perceptronPrint(attribute);// attribute is output stream > default is system out;
		}
		return instruction;
	}
	
	public void start(){
		//NB. if args was empty all the default actions have been loaded from properties file
		
		// load data file, path given by first argument
		data = HeadedTableFactory.create(args[0]); 
		
		/* execute arguments (second command needs to be -l or -m (load or make)
		 *  NB. saves are only made when -s is used
		 *  NB. defaults are only changed when -d is used
		 *  
		 *  an commandline of: 
		 *  src/datasetfile.cvs -mtest -t2 -s -osomefolder/path/output.file -v
		 *  
		 * above line will:
		 *  - load datasetfile datat
		 *  - make a header and perceptron representation and set internal name to "test"
		 *  - train the perceptron on the dataset 2 times
		 *  - save the header and perceptron as test.header and test.perceptron
		 *  FIXME: only outputs if file already exists
		 *  - output calculated the output of al the cases in the datasetfile to somefolder/path/output.file
		 *  - print the score of the perceptron on the testset
		 *  
		 *  These commands can be repeated and rearranged as long as the first is a -l or -m
		 *  	- somefolder/path/output.file -ltest -v -t -v -p -d -s -c10 -t3 -o -v -ssrc/newpath/newname
		 *  is also a valid statement
		 *  There are other commands as well read the switch statement below for usage options and help
		 *  
		 *  
		 */
		for(int i = 1; i<args.length; i++){
			char commando = args[i].charAt(1);
			String attribute = args[i].substring(2);
			i=execute(commando,attribute,i);
			i=Math.min(Math.max(1,i),args.length);
		}
	}	
	
	
	public static void main(String[] args) {
		if (args.length > 0){
			new Main(args).start();
		}
	}
	


}
